diff --git a/app/app.js b/app/app.js index 655be43b0..e54e9a501 100644 --- a/app/app.js +++ b/app/app.js @@ -79,6 +79,7 @@ const App = Application.extend({ 'digitalOcean', 'endpoint', 'github', + 'google', 'globalStore', 'intl', 'modal', diff --git a/app/models/googleoauthconfig.js b/app/models/googleoauthconfig.js new file mode 100644 index 000000000..70a1fa163 --- /dev/null +++ b/app/models/googleoauthconfig.js @@ -0,0 +1,5 @@ +import Resource from '@rancher/ember-api-store/models/resource'; + +var GoogleOauthConfig = Resource.extend({ type: 'googleOauthConfig', }); + +export default GoogleOauthConfig; diff --git a/app/styles/pages/_settings.scss b/app/styles/pages/_settings.scss index 5d4b19c58..f86b09900 100644 --- a/app/styles/pages/_settings.scss +++ b/app/styles/pages/_settings.scss @@ -67,6 +67,10 @@ &.freeipa { background-image: url('images/providers/provider-freeipa.svg'); } + + &.googleoauth { + background-image: url('images/providers/provider-google.svg'); + } } .advanced-setting { diff --git a/app/verify-auth/route.js b/app/verify-auth/route.js index b4d0fbf57..800ebedb7 100644 --- a/app/verify-auth/route.js +++ b/app/verify-auth/route.js @@ -12,6 +12,7 @@ const allowedForwards = ['localhost']; export default Route.extend(VerifyAuth, { github: service(), + google: service(), intl: service(), language: service('user-language'), @@ -23,6 +24,7 @@ export default Route.extend(VerifyAuth, { model(params/* , transition */) { const github = get(this, 'github'); + const google = get(this, 'google'); const code = get(params, 'code'); const forward = get(params, 'forward'); @@ -50,11 +52,13 @@ export default Route.extend(VerifyAuth, { if ( window.opener && !get(params, 'login') && !get(params, 'errorCode') ) { let openersGithub = window.opener.ls('github'); + let openersGoogle = window.opener.ls('google'); let openerStore = window.opener.ls('globalStore'); let qp = get(params, 'config') || get(params, 'authProvider'); let type = `${ qp }Config`; let config = openerStore.getById(type, qp); let gh = get(this, 'github'); + let go = get(this, 'google'); let stateMsg = 'Authorization state did not match, please try again.'; if ( get(params, 'config') === 'github' ) { @@ -63,6 +67,12 @@ export default Route.extend(VerifyAuth, { }).catch((err) => { this.send('gotError', err); }); + } else if ( get(params, 'config') === 'googleoauth') { + return go.testConfig(config).then((resp) => { + go.authorize(resp, openersGoogle.get('state')); + }).catch((err) => { + this.send('gotError', err) + }) } else if ( samlProviders.includes(get(params, 'config')) ) { if ( window.opener.window.onAuthTest ) { reply(null, config); @@ -72,7 +82,9 @@ export default Route.extend(VerifyAuth, { } if ( get(params, 'code') ) { - if ( openersGithub.stateMatches(get(params, 'state')) ) { + const currentOpener = openersGithub.state ? openersGithub : openersGoogle; + + if ( currentOpener.stateMatches(get(params, 'state')) ) { reply(params.error_description, params.code); } else { reply(stateMsg); @@ -87,11 +99,13 @@ export default Route.extend(VerifyAuth, { } } - if ( code && get(params, 'login') ) { - if ( github.stateMatches(get(params, 'state')) ) { - let ghProvider = get(this, 'access.providers').findBy('id', 'github'); + if ( code && get(params, 'login') || get(params, 'state').includes('login') ) { + let currentProvider = github.stateMatches(get(params, 'state')) ? 'github' : 'googleoauth' - return ghProvider.doAction('login', { + if ( github.stateMatches(get(params, 'state')) || google.stateMatches(get(params, 'state')) ) { + currentProvider = get(this, 'access.providers').findBy('id', currentProvider); + + return currentProvider.doAction('login', { code, responseType: 'cookie', description: C.SESSION.DESCRIPTION, diff --git a/lib/global-admin/addon/engine.js b/lib/global-admin/addon/engine.js index 8cfca7763..a7ac984d8 100644 --- a/lib/global-admin/addon/engine.js +++ b/lib/global-admin/addon/engine.js @@ -18,6 +18,7 @@ const Eng = Engine.extend({ 'digitalOcean', 'endpoint', 'github', + 'google', 'globalStore', 'intl', 'modal', diff --git a/lib/global-admin/addon/mixins/authentication.js b/lib/global-admin/addon/mixins/authentication.js index f6892c682..961b78a96 100644 --- a/lib/global-admin/addon/mixins/authentication.js +++ b/lib/global-admin/addon/mixins/authentication.js @@ -61,7 +61,7 @@ export default Mixin.create({ this.send('clearError'); const model = get(this, 'authConfig'); - const accessMode = get(model, 'id') === 'github' ? 'restricted' : 'unrestricted'; + const accessMode = get(model, 'id') === 'github' || 'googleoauth' ? 'restricted' : 'unrestricted'; setProperties(model, { enabled: false, diff --git a/lib/global-admin/addon/routes.js b/lib/global-admin/addon/routes.js index a12574837..6bf0e9dd6 100644 --- a/lib/global-admin/addon/routes.js +++ b/lib/global-admin/addon/routes.js @@ -69,6 +69,7 @@ export default buildRoutes(function() { this.route('adfs'); this.route('okta'); this.route('freeipa'); + this.route('googleoauth'); }); this.route('cloud-credentials', function() { diff --git a/lib/global-admin/addon/security/authentication/controller.js b/lib/global-admin/addon/security/authentication/controller.js index 4752bbf12..8086cf9a7 100644 --- a/lib/global-admin/addon/security/authentication/controller.js +++ b/lib/global-admin/addon/security/authentication/controller.js @@ -73,6 +73,13 @@ export default Controller.extend({ available: this.hasRecord('openldapconfig'), providerType: 'ldap', }, + { + route: 'security.authentication.googleoauth', + label: 'Google', + css: 'googleoauth', + available: this.hasRecord('googleoauthconfig'), + providerType: null, + }, // {route: 'security.authentication.shibboleth', label: 'Shibboleth', css: 'shibboleth', available: this.hasRecord('shibbolethconfig') }, ]; }), diff --git a/lib/global-admin/addon/security/authentication/github/template.hbs b/lib/global-admin/addon/security/authentication/github/template.hbs index e5f870d1f..19833e93c 100644 --- a/lib/global-admin/addon/security/authentication/github/template.hbs +++ b/lib/global-admin/addon/security/authentication/github/template.hbs @@ -147,4 +147,4 @@ {{/accordion-list-item}} {{/unless}} -{{/accordion-list}} +{{/accordion-list}} \ No newline at end of file diff --git a/lib/global-admin/addon/security/authentication/googleoauth/controller.js b/lib/global-admin/addon/security/authentication/googleoauth/controller.js new file mode 100644 index 000000000..7173a9bfa --- /dev/null +++ b/lib/global-admin/addon/security/authentication/googleoauth/controller.js @@ -0,0 +1,75 @@ +import { get, set, computed, setProperties } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import Controller from '@ember/controller'; +import AuthMixin from 'global-admin/mixins/authentication'; + +export default Controller.extend(AuthMixin, { + google: service(), + endpoint: service(), + access: service(), + settings: service(), + + confirmDisable: false, + errors: null, + testing: false, + error: null, + saved: false, + saving: false, + haveToken: false, + + organizations: null, + secure: true, + + authConfig: alias('model.googleConfig'), + isEnabled: alias('authConfig.enabled'), + + actions: { + save() { + this.send('clearError'); + set(this, 'saving', true); + + const authConfig = get(this, 'authConfig'); + const am = 'unrestricted'; + + setProperties(authConfig, { + 'oauthCredential': (authConfig.get('oauthCredential') || '').trim(), + 'serviceAccountCredential': (authConfig.get('serviceAccountCredential') || '').trim(), + 'adminEmail': (authConfig.get('adminEmail') || '').trim(), + 'hostname': (authConfig.get('hostname') || '').trim(), + 'enabled': false, + 'accessMode': am, + 'tls': true, + 'allowedPrincipalIds': [], + }); + + set(this, '_boundSucceed', this.authenticationApplied.bind(this)); + get(this, 'google').test(authConfig, get(this, '_boundSucceed')); + }, + }, + + destinationUrl: computed(() => { + return `${ window.location.origin }/`; + }), + + destinationDomain: computed(() => { + return `${ window.location.hostname }` + }), + + redirectURI: computed(() => { + return `${ window.location.origin }/verify-auth` + }), + + authenticationApplied(err) { + set(this, 'saving', false); + + if (err) { + set(this, 'isEnabled', false); + this.send('gotError', err); + + return; + } + + this.send('clearError'); + }, +}); diff --git a/lib/global-admin/addon/security/authentication/googleoauth/route.js b/lib/global-admin/addon/security/authentication/googleoauth/route.js new file mode 100644 index 000000000..2e0adb848 --- /dev/null +++ b/lib/global-admin/addon/security/authentication/googleoauth/route.js @@ -0,0 +1,31 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import { hash } from 'rsvp'; + +export default Route.extend({ + globalStore: service(), + + model() { + let gs = get(this, 'globalStore'); + + return hash({ + googleConfig: gs.find('authconfig', 'googleoauth', { forceReload: true }), + principals: gs.all('principal') + }).catch((e) => { + return e; + }) + }, + + setupController(controller, model) { + controller.setProperties({ + model, + confirmDisable: false, + testing: false, + organizations: get(this, 'session.orgs') || [], + errors: null, + }); + + controller.set('saved', true); + } +}); diff --git a/lib/global-admin/addon/security/authentication/googleoauth/template.hbs b/lib/global-admin/addon/security/authentication/googleoauth/template.hbs new file mode 100644 index 000000000..4559c8e82 --- /dev/null +++ b/lib/global-admin/addon/security/authentication/googleoauth/template.hbs @@ -0,0 +1,210 @@ +
+ {{#unless isEnabled}} + + {{/unless}} + {{top-errors errors=errors}} +
+ +{{#accordion-list showExpandAll=false as |al expandFn|}} + + {{#if isEnabled}} + {{#accordion-list-item + expand=(action expandFn) + expandAll=al.expandAll + expandOnInit=true + expanded=true + showExpand=false + title=(t 'authPage.google.authenticated.header.text') + }} +
+
+
+ +
+
+
+
{{t 'authPage.google.authenticated.header.adminEmail.text'}} {{authConfig.adminEmail}}
+
+ {{/accordion-list-item}} + + {{#accordion-list-item + classNames="mt-30" + detail=(t 'siteAccess.helpText' appName=settings.appName htmlSafe=true) + expand=(action expandFn) + expandAll=al.expandAll + expandOnInit=true + expanded=true + showExpand=false + title=(t 'siteAccess.header') + }} + {{site-access + model=authConfig + principals=model.principals + collection='siteAccess.organizations' + }} + {{/accordion-list-item}} + {{/if}} + + {{#unless isEnabled}} + {{#accordion-list-item + expand=(action expandFn) + expandAll=al.expandAll + expandOnInit=true + expanded=true + showExpand=false + title=(t 'authPage.google.notAuthenticated.header') + }} +
+

+

    +
  1. + {{t 'authPage.google.notAuthenticated.ul.li1.text' htmlSafe=true}} +
      +
    • {{t 'authPage.google.notAuthenticated.ul.li1.ul.li1'}}
    • +
    +
  2. +
  3. + {{t 'authPage.google.notAuthenticated.ul.li2.text'}} +
      +
    • {{t 'authPage.google.notAuthenticated.ul.li2.ul.li1' appName=settings.appName htmlSafe=true}} + {{destinationDomain}}{{copy-to-clipboard size='small' clipboardText=destinationUrl htmlSafe=true}} +
    • +
    • + {{t 'authPage.google.notAuthenticated.ul.li2.ul.li2' htmlSafe=true}} {{destinationUrl}}{{copy-to-clipboard size='small' clipboardText=destinationUrl htmlSafe=true}} +
    • +
    • {{t 'authPage.google.notAuthenticated.ul.li2.ul.li3'}}
    • +
    +
  4. +
  5. + {{t 'authPage.google.notAuthenticated.ul.li3.text'}} +
      +
    • {{t 'authPage.google.notAuthenticated.ul.li3.ul.li1'}} +
    • +
    • + {{t 'authPage.google.notAuthenticated.ul.li3.ul.li2' htmlSafe=true}} {{destinationUrl}}{{copy-to-clipboard size='small' clipboardText=destinationUrl htmlSafe=true}} +
    • +
    • {{t 'authPage.google.notAuthenticated.ul.li3.ul.li3' htmlSafe=true}} + {{redirectURI}}{{copy-to-clipboard size='small' clipboardText=destinationUrl htmlSafe=true}} +
    • +
    • {{t 'authPage.google.notAuthenticated.ul.li3.ul.li4'}}
    • +
    +
  6. +
  7. + {{t 'authPage.google.notAuthenticated.ul.li4.text'}} +
      +
    • {{t 'authPage.google.notAuthenticated.ul.li4.ul.li1'}} +
    • +
    • {{t 'authPage.google.notAuthenticated.ul.li4.ul.li2'}}
    • +
    • {{t 'authPage.google.notAuthenticated.ul.li4.ul.li3'}}
    • +
    +
  8. +
+

+
+ {{/accordion-list-item}} + + {{#accordion-list-item + expand=(action expandFn) + expandAll=al.expandAll + expandOnInit=true + expanded=true + showExpand=false + title=(t 'authPage.google.notAuthenticated.form.header' appName=settings.appName) + }} +
+
+ +
+
+
+ + {{input type="text" name="username" value=authConfig.adminEmail classNames="form-control"}} +

{{t 'authPage.google.notAuthenticated.form.adminEmail.helperText'}}

+
+
+
+
+ + {{input type="text" value=authConfig.hostname classNames="form-control"}} +

{{t 'authPage.google.notAuthenticated.form.hostname.helperText'}}

+
+
+
+ +
+
+ {{#input-text-file + classNames="box" + label="authPage.google.notAuthenticated.form.oauthCredential.labelText" + value=authConfig.oauthCredential + accept="text/*, .json" + minHeight=60 + canChangeName=false + nameRequired=true + placeholder="authPage.google.notAuthenticated.form.oauthCredential.labelText" + concealValue=true + as |section| + }} + {{#if (eq section "description")}} +
+
+ {{t "authPage.google.notAuthenticated.form.oauthCredential.helperText" htmlSafe=true}} +
+
+ {{/if}} + {{/input-text-file}} +
+
+ +
+
+ {{#input-text-file + classNames="box" + label="authPage.google.notAuthenticated.form.serviceAccountCredential.labelText" + value=authConfig.serviceAccountCredential + accept="text/*, .json" + minHeight=60 + canChangeName=false + nameRequired=true + placeholder="authPage.google.notAuthenticated.form.serviceAccountCredential.labelText" + concealValue=true + as |section| + }} + {{#if (eq section "description")}} +
+
+ {{t "authPage.google.notAuthenticated.form.serviceAccountCredential.helperText" htmlSafe=true}} +
+
+ {{/if}} + {{/input-text-file}} +
+
+ +
+
+ + +
+
+ +
+
+ {{/accordion-list-item}} + {{/unless}} +{{/accordion-list}} \ No newline at end of file diff --git a/lib/global-admin/app/security/authentication/googleoauth/controller.js b/lib/global-admin/app/security/authentication/googleoauth/controller.js new file mode 100644 index 000000000..f31618756 --- /dev/null +++ b/lib/global-admin/app/security/authentication/googleoauth/controller.js @@ -0,0 +1 @@ +export { default } from 'global-admin/security/authentication/googleoauth/controller'; diff --git a/lib/global-admin/app/security/authentication/googleoauth/route.js b/lib/global-admin/app/security/authentication/googleoauth/route.js new file mode 100644 index 000000000..afdf6757d --- /dev/null +++ b/lib/global-admin/app/security/authentication/googleoauth/route.js @@ -0,0 +1 @@ +export { default } from './global-admin/security/authentication/googleoauth/route'; diff --git a/lib/login/addon/components/login-google/component.js b/lib/login/addon/components/login-google/component.js new file mode 100644 index 000000000..a4d197dc5 --- /dev/null +++ b/lib/login/addon/components/login-google/component.js @@ -0,0 +1,12 @@ +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; + +export default Component.extend({ + google: service(), + + actions: { + authenticate() { + this.get('google').login(); + } + } +}); diff --git a/lib/login/addon/components/login-google/template.hbs b/lib/login/addon/components/login-google/template.hbs new file mode 100644 index 000000000..e8e118063 --- /dev/null +++ b/lib/login/addon/components/login-google/template.hbs @@ -0,0 +1,5 @@ +
+ +
diff --git a/lib/login/addon/login/controller.js b/lib/login/addon/login/controller.js index c2c4188e1..f9ee97cef 100644 --- a/lib/login/addon/login/controller.js +++ b/lib/login/addon/login/controller.js @@ -150,6 +150,10 @@ export default Controller.extend({ return !!get(this, 'access.providers').findBy('id', 'github'); }), + isGoogle: computed('access.providers', function() { + return !!get(this, 'access.providers').findBy('id', 'googleoauth'); + }), + isPing: computed('access.providers', function() { return !!get(this, 'access.providers').findBy('id', 'ping'); }), diff --git a/lib/login/addon/login/template.hbs b/lib/login/addon/login/template.hbs index 8e0e8cce9..23b321e59 100644 --- a/lib/login/addon/login/template.hbs +++ b/lib/login/addon/login/template.hbs @@ -31,6 +31,10 @@ {{login-github action=(action "started")}} {{/if}} + {{#if isGoogle}} + {{login-google action=(action "started")}} + {{/if}} + {{#if isShibboleth}}

{{t "loginPage.shibbolethMessage" appName=settings.appName}}


diff --git a/lib/shared/addon/google/service.js b/lib/shared/addon/google/service.js new file mode 100644 index 000000000..cd30c1c4a --- /dev/null +++ b/lib/shared/addon/google/service.js @@ -0,0 +1,148 @@ +import Service, { inject as service } from '@ember/service'; +import { addQueryParam, addQueryParams, popupWindowOptions } from 'shared/utils/util'; +import { get, set } from '@ember/object'; +import C from 'shared/utils/constants'; + +const googleOauthScope = 'openid profile email https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly' + +export default Service.extend({ + access: service(), + cookies: service(), + session: service(), + globalStore: service(), + app: service(), + intl: service(), + + generateState() { + return set(this, 'session.googleState', `${ Math.random() }`); + }, + + generateLoginStateKey() { + return set(this, 'session.googleState', `${ Math.random() }login`) + }, + + stateMatches(actual) { + return actual && get(this, 'session.googleState') === actual; + }, + + testConfig(config) { + return config.doAction('configureTest', config); + }, + + saveConfig(config, opt) { + return config.doAction('testAndApply', opt); + }, + + authorize(auth, state) { + const url = addQueryParams(get(auth, 'redirectUrl'), { + scope: googleOauthScope, + redirect_uri: `${ window.location.origin }/verify-auth`, + state, + }); + + + return window.location.href = url; + }, + + login(forwardUrl) { + const provider = get(this, 'access.providers').findBy('id', 'googleoauth'); + const authRedirect = get(provider, 'redirectUrl'); + let redirect = `${ window.location.origin }/verify-auth`; + + if ( forwardUrl ) { + redirect = addQueryParam(redirect, 'forward', forwardUrl); + } + + const url = addQueryParams(authRedirect, { + scope: googleOauthScope, + state: this.generateLoginStateKey(), + redirect_uri: redirect, + }); + + window.location.href = url; + }, + + test(config, cb) { + let responded = false; + + window.onAuthTest = (err, code) => { + if ( !responded ) { + let googleConfig = config; + + responded = true; + + this.finishTest(googleConfig, code, cb); + } + }; + + set(this, 'state', this.generateState()); + + let url = addQueryParams(`${ window.location.origin }/verify-auth`, { config: 'googleoauth', }); + + const popup = window.open(url, 'rancherAuth', popupWindowOptions()); + const intl = get(this, 'intl'); + + let timer = setInterval(() => { + if (popup && popup.closed ) { + clearInterval(timer); + + if ( !responded ) { + responded = true; + cb({ + type: 'error', + message: intl.t('authPage.google.testAuth.authError') + }); + } + } else if (popup === null || typeof (popup) === 'undefined') { + clearInterval(timer); + + if ( !responded ) { + responded = true; + + cb({ + type: 'error', + message: intl.t('authPage.google.testAuth.popupError') + }); + } + } + }, 500); + }, + + finishTest(config, code, cb) { + const goConfig = config; + + set(goConfig, 'enabled', true); + + let out = { + code, + enabled: true, + googleOauthConfig: goConfig, + description: C.SESSION.DESCRIPTION, + ttl: C.SESSION.TTL, + }; + + const allowedPrincipalIds = get(config, 'allowedPrincipalIds') || []; + + return this.saveConfig(config, out).then(() => { + let found = false; + const myPIds = get(this, 'access.me.principalIds'); + + myPIds.forEach( (id) => { + if (allowedPrincipalIds.indexOf(id) >= 0) { + found = true; + } + }); + + if ( !found && !allowedPrincipalIds.length) { + allowedPrincipalIds.pushObject(get(this, 'access.principal.id')); + } + + return goConfig.save().then(() => { + window.location.href = window.location.href; + }); + }) + .catch((err) => { + cb(err); + }); + }, +}) diff --git a/lib/shared/addon/pipeline-github/service.js b/lib/shared/addon/pipeline-github/service.js index 4a64e7984..ee916c38f 100644 --- a/lib/shared/addon/pipeline-github/service.js +++ b/lib/shared/addon/pipeline-github/service.js @@ -65,4 +65,4 @@ export default Service.extend({ } }, 500); }, -}); \ No newline at end of file +}); diff --git a/lib/shared/app/google/service.js b/lib/shared/app/google/service.js new file mode 100644 index 000000000..e8bf42af0 --- /dev/null +++ b/lib/shared/app/google/service.js @@ -0,0 +1 @@ +export { default } from 'shared/google/service'; diff --git a/public/assets/images/providers/provider-google.svg b/public/assets/images/providers/provider-google.svg new file mode 100644 index 000000000..90064d7de --- /dev/null +++ b/public/assets/images/providers/provider-google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 2cd115699..a01073e7c 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -520,6 +520,68 @@ authPage: post: Waiting to hear back from GitHub authError: 'Github access was not authorized' popupError: 'Please disable your pop-up blocker and click "Authenticate" again.' + google: + header: + disabled: + label: 'Google is not configured' + authenticated: + header: + text: Authentication + adminEmail: + text: "Admin Email: " + disableAccess: + header: "Danger Zone™" + warning: 'Caution: Disabling access control will give complete control over {appName} to anyone that can reach this page or the API.' + confirmDisable: "Are you sure? Click again to disable access control" + disable: Disable Google access + notAuthenticated: + header: "1. Configure your Google Application settings" + ul: + li1: + text: 'For standard Google, click here to go to your Google developer console.' + ul: + li1: 'Login to your account. Navigate to "APIs & Services" and then select "Credentials".' + li2: + text: 'Navigate to the "OAuth consent screen" tab and fill in the form:' + ul: + li1: 'Authorized domains: ' + li2: 'Application homepage link: ' + li3: 'Enable "email", "profile", and "openid" to Scopes for Google APIs.' + li4: 'Authorization callback URL:' + li3: + text: 'Navigate to the "Credentials" tab to create your OAuth client ID:' + ul: + li1: 'Select "Create Credentials", select OAuth clientID, then select Web application.' + li2: 'Authorized Javascript origins: ' + li3: 'Authorized redirect URIs: ' + li4: 'Click "save" and then download JSON.' + li4: + text: 'Navigate to the "Credentials" tab again to create your Service account key:' + ul: + li1: 'Select your service account.' + li2: 'Select JSON for your Key type and then click "Create".' + li3: 'A JSON file will be automatically saved locally.' + form: + header: '2. Configure {appName} to use your application for authentication' + oauthCredential: + labelText: OAuth Credentials + helperText: Copy and paste in the OAuth Credentials JSON which can be found in your Google API developers console (Step 3 above). + serviceAccountCredential: + labelText: Service Account Credentials + helperText: Copy and paste in the Service Account Credentials JSON which can be found in the service accounts section of the Google API developers console (Step 4 above). + adminEmail: + labelText: Admin Email + helperText: Enter the Admin Email associated with your account. + hostname: + labelText: Host name + helperText: Enter the host name of your account. + testAuth: + buttonText: + pre: Authenticate with Google + post: Waiting to hear back from Google + authError: 'Google access was not authorized' + popupError: 'Please disable your pop-up blocker and click "Authenticate" again.' + azuread: header: disabled: @@ -5622,6 +5684,9 @@ loginShibboleth: loginGithub: buttonText: Log In with GitHub +loginGoogle: + buttonText: Log In with Google + loginAzure: buttonText: Log In with Azure AD