UNPKG

vue-loginservice

Version:
999 lines (924 loc) 33.6 kB
/* @flZZow */ /* * Client API for LoginService.io * See https://authservice.io */ // import Vue from 'vue' // import Vuex from 'vuex' // Vue.use(Vuex) //import { install } from './install' import jwtDecode from 'jwt-decode' import axios from 'axios' import QueryString from 'query-string' import { assert, inBrowser } from './misc' console.log('(LoginService.js) 1') // const debug = process.env.NODE_ENV !== 'production' console.log('(LoginService.js) 2') const JWT_COOKIE_NAME = 'authservice-jwt' const LOGIN_TIMEOUT_DAYS = 3 const NETWORK_ERROR_MSG = 'Could not contact authentication server' class LoginService { // static install: (Vue) => void; // static version: string; // static install (Vue) { // alert('Install 2...') // // Vue.prototype.$auth = new LoginService() // Vue.prototype.$auth = 123 // // Object.defineProperty(Vue.prototype, '$authservice', { // get () { return 987 } // }) // } constructor (options) { console.log('&&& LoginService constructor', options) this.host = options.host ? options.host : 'api.authservice.io' this.port = options.port ? options.port : 443 this.version = options.version ? options.version : 'v2' this.apikey = options.apikey // Determine what protocol to use this.protocol = 'http' if (options.protocol && (options.protocol==='http' || options.protocol==='https')) { this.protocol = options.protocol } else { // See if we can determine the protocol from the port if (this.port === 80) { this.protocol = 'http' } else if (this.port === 443) { this.protocol = 'https' } else { // Non-standard port, probably during development } } // Decide which icon set to use with a defaultIconPack option. // Loosely based on: // https://buefy.github.io/#/documentation/constructor-options // // Currently recognise: // fa (font-awsome 4) // fas (font-awsome 5) this.defaultIconPack = options.defaultIconPack ? options.defaultIconPack : 'fa' this.icons = (pack) => { return this.defaultIconPack === pack } // See if we are supporting email login (default to yes) if (options.hints && options.hints.login && typeof(options.hints.login.email) !== 'undefined' && !options.hints.login.email) { console.log(`LoginService(): Email is NOT enabled`); this.emailSupported = false; } else { console.log(`LoginService(): Email IS enabled`); this.emailSupported = true; } // See if we have URLs specified for successful login and failure. // If these are not set, these events will return to the initiating page. this.loginResume = null this.loginFail = null if (options.hints && options.hints.login) { // See if we have a resumeURL if (options.hints.login.resumeURL) { if (typeof(options.hints.login.resumeURL) !== 'string') { console.error('options.hints.login.resumeURL must be a string') console.log(`Will return to current page after login`); } else { // Convert our resumeURL to a full URL (it is usually a relative path) this.loginResume = this.URLOnThisWebsite(options.hints.login.resumeURL) if (this.loginResume === null) { // The resumeURL is absolute, but not to the current website's domain. console.error(`options.hints.login.resumeURL not on this website: (${options.hints.login.resumeURL})`) console.log(`Will return to current page after login`); } } } // See if we have a failURL if (options.hints.login.failURL) { if (typeof(options.hints.login.failURL) !== 'string') { console.error('options.hints.login.failURL must be a string') console.log(`Will return to current page on error`); } else { // Convert our failURL to a full URL (it is usually a relative path) this.loginFail = this.URLOnThisWebsite(options.hints.login.failURL) if (this.loginFail === null) { // The failURL is absolute, but not to the current website's domain. console.error(`options.hints.login.failURL not on this website: (${options.hints.login.failURL})`) console.log(`Will return to current page on error`); } } } } // See if registration is allowed if (!this.emailSupported) { // login.email: false console.log(`LoginService(): Registration is NOT supported`); console.log(`(because email is not supported)`) this.registrationSupported = false; } else if (options.hints && typeof(options.hints.register) !== 'undefined' && !options.hints.register) { // Check for hints.register: false console.log(`LoginService(): Registration is NOT supported`); this.registrationSupported = false; } else { // We WILL allow registration. Check we have what we need. if (typeof(options.hints.register) !== 'object') { this.registrationSupported = false; console.error('options.hints.register must be false, or an object') } else if (!options.hints.register.resumeURL) { this.registrationSupported = false; console.log(`LoginService(): Registration is NOT supported`); console.error('options.hints.register.resumeURL must be provided') } else if (typeof(options.hints.register.resumeURL) !== 'string') { this.registrationSupported = false; console.log(`LoginService(): Registration is NOT supported`); console.error('options.hints.register.resumeURL must be a string') } else { // Convert our resumeURL to a full URL (it is usually a relative path) this.registerResume = this.URLOnThisWebsite(options.hints.register.resumeURL) if (this.registerResume === null) { // The resumeURL is absolute, but not to the current website's domain. console.error(`LoginService(): Registration will NOT be supported`); console.log(`options.hints.register.resumeURL not on this website: (${options.hints.register.resumeURL})`) this.registrationSupported = false; } else { // All good for registration console.log(`LoginService(): Registration enabled`); this.registrationSupported = true; } } } // See if forgotten password is allowed if (!this.emailSupported) { // Email is not used (options.login.email is false) console.error(`LoginService(): Forgotten password will NOT be supported`); console.log(`(because email is not supported)`) this.forgottenPasswordSupported = false; } else if (options.hints && typeof(options.hints.forgot) !== 'undefined' && !options.hints.forgot) { // Forgot password is specifically disallowed (options.hints.register is false) this.forgottenPasswordSupported = false; } else { // We WILL allow forgot password. Check we have what we need. if (typeof(options.hints.forgot) !== 'object') { console.error(`LoginService(): Forgotten password will NOT be supported`); console.log('options.hints.forgot must be false, or an object') this.forgottenPasswordSupported = false; } else if (!options.hints.forgot || !options.hints.forgot.resumeURL) { console.error(`LoginService(): Forgotten password will NOT be supported`); console.log('options.hints.forgot.resumeURL must be provided') this.forgottenPasswordSupported = false; } else if (typeof(options.hints.forgot.resumeURL) !== 'string') { console.error(`LoginService(): Forgotten password will NOT be supported`); console.log('options.hints.forgot.resumeURL must be a string') this.forgottenPasswordSupported = false; } else { // Convert our resumeURL to a full URL (it is usually a relative path) this.forgotResume = this.URLOnThisWebsite(options.hints.forgot.resumeURL) if (this.forgotResume === null) { // The resumeURL is absolute, but not to the current website's domain. console.error(`LoginService(): Forgotten password will NOT be supported`); console.log(`options.hints.forgot.resumeURL not on this website: (${options.hints.forgot.resumeURL})`) this.forgottenPasswordSupported = false; } else { // All good for forgotten password console.log(`LoginService(): Forgotten password enabled`); this.forgottenPasswordSupported = true; } } } // Remember the options this.options = options // Current user details this.user = null this.jwt = null this.fromCache = false } // init (app: any /* Vue component instance */) { init (app /* Vue component instance */) { console.log('&&& MyComponent init') // VVVVV This does not seem to be called // alert('za init()') process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(LoginService, options)\` ` + `before creating root instance.` ) } // Work out the server's endpoint endpoint () { let portStuff = '' if (this.protocol === 'http' && this.port !== 80) { portStuff = `:${this.port}` } else if (this.protocol === 'https' && this.port !== 443) { portStuff = `:${this.port}` } const endpoint = `${this.protocol}://${this.host}${portStuff}/${this.version}/${this.apikey}` return endpoint } // // We've just arrived at a page. // See if a JWT for the current user is provided in the URL parameters // or in a cookie for this site. // checkInitialLoginStatus (debug) { debug = true console.log('+++++ checkInitialLoginStatus ++++++') // If this is the browser, look for the JWT as a URL parameter if (window) { // See if we have AUTHSERVICE_JWT in the URL to this page let jwt = this.getURLParameterByName("AUTHSERVICE_JWT") if (jwt) { if (debug) { console.log("***") console.log("***") console.log("*** LOGINSERVICE_JWT IN URL") console.log("***") console.log("***") } const isFromCookie = false if (this.setCurrentUserFromJWT(jwt, isFromCookie)) { // Remember this JWT in a cookie for the next page. this.setCookie(JWT_COOKIE_NAME, jwt, LOGIN_TIMEOUT_DAYS) return true } else { // Invalid JWT this.removeCookie(JWT_COOKIE_NAME) return false } } } // See if we have a cookie containing the current JWT let jwt = this.getCookie(JWT_COOKIE_NAME) if (jwt) { if (debug) { console.log("***") console.log("***") console.log("*** LOGINSERVICE_JWT IN A COOKIE") console.log("***") console.log("***") } // var isFromCookie = true; const isFromCookie = false // Check if it is stale ZZZZ if (this.setCurrentUserFromJWT(jwt, isFromCookie)) { // Good login from cookie return true } else { // Dud cookie this.removeCookie(JWT_COOKIE_NAME) return false } } // not a good cookie if (debug) { console.log("***") console.log("***") console.log("*** LOGINSERVICE_JWT NOT IN URL OR COOKIE") console.log("***") console.log("***") } const isFromCookie = false this.setCurrentUserFromJWT(null, isFromCookie) return false } /* * Log in using username / password. */ login (email, password) { return new Promise((resolve, reject) => { console.log('login(email=' + email + ')') // console.log('++++++++++ email=' + email + ', password=' + password) /* * Call the server to authenticate the username/password. */ axios({ method: 'post', url: this.endpoint() + '/email/login', headers: { // 'Authorization': 'Bearer ' + jwt 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: { email: email, password: password } }) .then(response => { // JSON responses are automatically parsed. // this.posts = response.data // console.log('axios reply (response.data)=', response.data) // return callback(null, response.data.data[0]) if (response.data.status === 'ok') { // Logged in. // console.log('Back from login:', response.data) const jwt = response.data.jwt const isFromCookie = false if (this.setCurrentUserFromJWT(jwt, isFromCookie)) { // Good JWT login this.setCookie(JWT_COOKIE_NAME, jwt, LOGIN_TIMEOUT_DAYS) resolve(this.user.id) return } else { console.log('ok 4') // Bad JWT this.removeCookie(JWT_COOKIE_NAME) console.log('ok 5') reject('Invalid credentials') return } } else { // We did not sucessfully login // -> No current user const isFromCache = false this.setCurrentUserFromJWT(null, isFromCache) this.removeCookie(JWT_COOKIE_NAME) reject(response.data.message) return } }) .catch(e => { // We did not sucessfully login // => No current user const isFromCache = false this.setCurrentUserFromJWT(null, isFromCache) this.removeCookie(JWT_COOKIE_NAME) console.log(`e=`, e); console.log(`e.response:`, e.response); console.log(`e.status:`, e.status); if (!e.response) { // Network error from browser // See https://github.com/axios/axios/issues/383#issuecomment-234079506 reject(NETWORK_ERROR_MSG) return } else { console.log(`e:`, e); console.log(`e.response:`, e.response); console.log(`e.data:`, e.data); reject(e.response.data.message) return } }) })// promise }// - login() /* * Kick off the OAuth2 login process. */ initiateOAuth (me, authority) { console.log(`initiateOAuth(me, ${authority})`) // Where to go if the login succeeds? let resumeURL if (this.loginResume) { // Use the specified resume URL (which is a relative path) resumeURL = this.URLOnThisWebsite(this.loginResume) } else { // The 'resume' URL has not been provided, so we'll come back to this // exact same URL, however with any JWT or error parameters removed. const l = window.location console.log('resume to current page after oauth login', l) const parsed = QueryString.parse(l.search) delete parsed['LOGINSERVICE_JWT'] delete parsed['AUTHSERVICE_ERROR'] const params = QueryString.stringify(parsed) // console.log(parsed) // Contruct the URL, with the adjusted parameters resumeURL = l.protocol + "//" + l.host + l.pathname if (params) { resumeURL += '?' + params } resumeURL += l.hash console.log('\n\nresumeURL=', resumeURL) console.log(new Buffer(resumeURL).toString('base64')) } // Get the URL to a "bounce page". This is a page that sets the JWT // cookie from a URL parameter, and then redirects to the 'resume' page. const resume64 = new Buffer(resumeURL).toString('base64') const params = QueryString.stringify({ next: resume64 }) const hash = '' const bounceURL = this.URLOnThisWebsite(`/authservice-bounce?${params}#${hash}`) console.log('\n\nbounceURL=', bounceURL) const successURL = bounceURL // Where to go if the login fails? var failURL if (this.loginFail) { // Use the specified error URL (which is a relative path) failURL = this.loginFail } else { failURL = bounceURL } console.log('successURL=' + successURL) console.log('successURL=' + encodeURIComponent(successURL)) console.log('failURL=' + failURL) console.log('failURL=' + encodeURIComponent(failURL)) let url = `${protocol}://${this.host}:${this.port}/${this.version}/oauth2/initiate/${this.apikey}/${authority}` url += '?success=' + encodeURIComponent(successURL) url += '&fail=' + encodeURIComponent(failURL) // alert('Initiate URL:' + url) window.location = url }// initiateOAuth2 /* * Log out */ logout () { return new Promise((resolve, reject) => { // VVVVV Call the server var isFromCache = false this.setCurrentUserFromJWT(null, isFromCache) this.removeCookie(JWT_COOKIE_NAME) resolve(0) return })// new Promise } register (options) { console.log('$authservice.register()', options); return new Promise((resolve, reject) => { console.log('INside the promise:', reject) if (!this.registrationSupported) { const error = 'Registration is not available.' reject(error) return } // let email = options.email // let username = options.username // let password = options.password // let firstName = options.firstName // let middleName = options.middleName // let lastName = options.lastName // let resume = options.resume // Check email and password is valid switch (typeof (options.email)) { case 'string': if (options.email.indexOf('@') < 1) { reject('Please enter a valid email address') } break case 'undefined': return reject('options.email must be provided') default: return reject('options.email must be a string') } // Prepare the parameters to the API call var params = { email: options.email, resume: this.registerResume } // Maybe check username is valid console.log('username is ' + options.username) switch (typeof (options.username)) { case 'string': const username = options.username.trim().toLowerCase() if (username.indexOf(' ') >= 0) { reject('Username may not contain spaces') return } if (username.indexOf('@') >= 0) { reject('Username may not contain @') return } params.username = username break case 'undefined': // alert('using email for username') params.username = params.email break default: return reject('If provided, options.username must be a string') } // if (me.registerRequiresUsername) { // username = username.trim().toLowerCase() // if (username.indexOf(' ') >= 0) { // return failCallback('Username may not contain spaces') // } else if (username.indexOf('@') >= 0) { // return failCallback('Username may not contain @') // } // params.username = username // } else { // params.username = email // } // Maybe check password is valid switch (typeof (options.password)) { case 'string': if (options.password.length < 5) { return reject('Please enter a longer password') } params.password = options.password break case 'undefined': break default: return reject('If provided, options.password must be a string') } // Maybe check first name is valid switch (typeof (options.firstName)) { case 'string': if (options.firstName.length < 1) { return reject('Please enter a first name') } params.firstName = options.firstName break case 'undefined': break default: return reject('If provided, options.firstName must be a string') } // Maybe check middle name is valid switch (typeof (options.middleName)) { case 'string': if (options.middleName.length < 1) { return reject('Please enter a middle name') } params.middleName = options.middleName break case 'undefined': break default: return reject('If provided, options.middleName must be a string') } // Maybe check last name is valid switch (typeof (options.lastName)) { case 'string': if (options.lastName.length < 1) { return reject('Please enter a last name') } params.lastName = options.lastName break case 'undefined': break default: return reject('If provided, options.lastName must be a string') } // Call the server console.log('params=', params) axios({ method: 'put', url: this.endpoint() + '/email/register', headers: { // 'Authorization': 'Bearer ' + jwt 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: params }) .then(response => { // JSON responses are automatically parsed. console.log('response is ', response) if (response.data && response.data.status === 'ok') { // If we have a new JWT, re-set the current user if (response.jwt) { var jwt = response.jwt var isFromCookie = false if (this.setCurrentUserFromJWT(jwt, isFromCookie)) { // Good registration this.setCookie(JWT_COOKIE_NAME, jwt, LOGIN_TIMEOUT_DAYS) } else { // All okay, but no auto-login in from registration this.removeCookie(JWT_COOKIE_NAME) } } resolve(jwt) return } else { // Display an error message const error = (response.data && response.data.message) ? response.data.message : 'Error while registering' reject(error) return } }) .catch(e => { if (!e.response) { // Network error from browser // See https://github.com/axios/axios/issues/383#issuecomment-234079506 reject(NETWORK_ERROR_MSG) return } else { // Error registering const error = (e.response.data && e.response.data.Error) ? e.response.data.Error : 'Error while registering' reject(error) return } }) })// new Promise }// register() forgot (email, options) { return new Promise((resolve, reject) => { if (!this.forgottenPasswordSupported) { const error = 'Password retrieval is not available.' reject(error) return } // Check email and password is valid if (email === null || email.indexOf('@') < 1) { const error = 'Please enter a valid email address' reject(error) return } // Get the URL to a "bounce page". This is a page that sets the JWT // cookie from a URL parameter, and then redirects to the 'resume' page. const resume64 = new Buffer(this.forgotResume).toString('base64') const params = QueryString.stringify({ next: resume64 }) const bounceURL = this.URLOnThisWebsite(`/authservice-bounce?${params}`) // console.log(`this.forgotResume=${this.forgotResume}`) // console.log('bounceURL=', bounceURL) // Call the server //console.log('params=', params) axios({ method: 'post', url: this.endpoint() + '/email/forgot', headers: { // 'Authorization': 'Bearer ' + jwt 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: { email: email, resume: bounceURL } }) .then(response => { // JSON responses are automatically parsed. if (response.data.status === 'ok') { // Email sent successfully resolve(response.data) return } else { // Error sending the email const error = (response.data && response.data.message) ? response.data.message : 'Error while sending email' reject(error) return } }) .catch(e => { if (!e.response) { // Network error from browser // See https://github.com/axios/axios/issues/383#issuecomment-234079506 reject(NETWORK_ERROR_MSG) return } else { // Error sending the email const error = (e.response.data && e.response.data.message) ? e.response.data.message : 'Error while sending email' reject(error) return } }) }) }// - forgot() // // Get a URL parameter. // getURLParameterByName (name) { var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search) return match && decodeURIComponent(match[1].replace(/\+/g, ' ')) } // Set the current user from a JWT. // Does not change cookies. // Returns true on success. setCurrentUserFromJWT (jwt, fromCookie) { // console.log() // console.log('++++++++> setCurrentUserFromJWT(): jwt=' + jwt) let haveUser = false let ident = null if (jwt) { // See https://github.com/auth0/jwt-decode try { var decoded = jwtDecode(jwt) console.log('decoded=', decoded) ident = decoded.identity haveUser = true } catch (e) { console.log('Error decoding JWT: ', e) // alert('Error decoding invalid JWT') haveUser = false } } // Change the current user. // let oldCurrentUser = user if (haveUser) { const user = { tenant: ident.tenant, authority: ident.authority, avatar: ident.avatar, email: ident.email, emailStatus: ident.email_status, entityType: ident.entity_type, firstname: ident.first_name, fullname: ident.full_name, gender: ident.gender, id: ident.id, isAdmin: ident.is_admin, languages: ident.languages, lastname: ident.last_name, locale: ident.locale, location: ident.location, mediaPage: ident.media_page, middlename: ident.middle_name, privileges: ident.privileges, status: ident.status, timezone: ident.timezone, username: ident.username, rights: [ ] // type: ident.type, } if (ident.rights) { ident.rights.forEach(r => { const right = { realm: r.realm, name: r.name, sequence: r.sequence, value: r.value } user.rights.push(right) }) } else { console.error(`JWT does not contain field {rights}.`) } // console.log('Setting user to ', user) this.user = user this.jwt = jwt this.fromCache = fromCookie return true } else { // No longer logged in this.user = null this.jwt = null this.fromCache = false return false } }// setCurrentUserFromJWT // See if a username is available usernameAvailability (username) { // console.log('usernameAvailability()', username) return new Promise((resolve, reject) => { // Check the length of the username username = username.trim().toLowerCase() if (username.length < 3) { reject('Username must be 3 or more characters') return } // Ask the server if the username is in use var url = this.endpoint() + '/username-availability/' + encodeURIComponent(username) // console.log('url=', url) axios({ method: 'get', url: url, headers: { // 'Authorization': 'Bearer ' + jwt 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { // JSON responses are automatically parsed. if (response.data.status === 'available') { // Name is available resolve(null) return } else { // Name not available resolve(response.data.error) return } }) .catch(e => { if (!e.response) { // Network error from browser // See https://github.com/axios/axios/issues/383#issuecomment-234079506 reject(NETWORK_ERROR_MSG) return } else { // alert('Communications error: unable to determine if this username is available') const error = e.response.data.Error ? e.response.data.Error : 'Could not check availability' reject(error) return } }) })// new Promise } // See if an email address is already used emailAvailability (email) { console.log('emailAvailability()', email) return new Promise((resolve, reject) => { // Check the length of the username //username = username.trim().toLowerCase() // if (username.length < 3) { // reject('Username must be 3 or more characters') // return // } // Ask the server if the username is in use var url = this.endpoint() + '/email-availability/' + encodeURIComponent(email) // console.log('url=', url) axios({ method: 'get', url: url, headers: { // 'Authorization': 'Bearer ' + jwt 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { // JSON responses are automatically parsed. if (response.data.status === 'available') { // Name is available resolve(null) return } else { // Name not available resolve(response.data.error) return } }) .catch(e => { if (!e.response) { // Network error from browser // See https://github.com/axios/axios/issues/383#issuecomment-234079506 reject(NETWORK_ERROR_MSG) return } else { // alert('Communications error: unable to determine if this username is available') const error = e.response.data.Error ? e.response.data.Error : 'Could not check availability' reject(error) return } }) })// new Promise } /* * Cookie handling */ setCookie (cname, cvalue, exdays) { // console.log('setCookie(' + cname + ', ' + cvalue + ')') if (cvalue) { console.log('setting cookie (' + cname + ')') } else { console.log('clearing cookie (' + cname + ')') } var d = new Date() d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)) var expires = 'expires=' + d.toUTCString() document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/' }// setCookie() getCookie (cname) { // console.log('getCookie(' + cname + ')') var name = cname + "=" var ca = document.cookie.split(';') for (var i = 0; i < ca.length; i++) { var c = ca[i] while (c.charAt(0) === ' ') { c = c.substring(1) } if (c.indexOf(name) === 0) { // console.log('- found cookie') return c.substring(name.length, c.length) } } // console.log('- no cookie with this name') return "" }// getCookie() removeCookie (cname) { // console.log('removeCookie(' + cname + ')') this.setCookie(cname, null, 0) }// removeCookie() isEmailSupported () { return this.emailSupported } isRegistrationSupported () { return this.registrationSupported } isForgottenPasswordSupported () { return this.forgottenPasswordSupported } URLOnThisWebsite (relativeURL) { //console.error(`URLOnThisWebsite(${relativeURL})`) //console.log(`Relative: ${relativeURL}`) // Work out the URI part of the current page const l = window.location let baseURL = `${l.protocol}//${l.hostname}` if (l.port) { if (l.protocol === 'http' && l.port === 80) { // default port } else if (l.protocol === 'https' && l.port === 443) { // default port } else { baseURL += `:${l.port}` } } //console.log('\n\nbaseURL=', baseURL) // Perhaps a full URL has been provided already if (relativeURL.startsWith('http')) { // Check it's on this website if (relativeURL.startsWith(baseURL)) { return relativeURL } console.log(`URL ${relativeURL} is not on current website ${baseURL}`) return null } // Add on the path if (!relativeURL.startsWith('/')) { baseUrl += '/' } const absoluteURL = baseURL + relativeURL //console.log(`Absolute: ${absoluteURL}`) return absoluteURL } } console.log('(LoginService.js) 3') //LoginService.install = install // The imported install() LoginService.version = '__VERSION__' //if (inBrowser && window.Vue) { // console.log('(LoginService.js) 4') // window.Vue.use(LoginService) //} console.log('(LoginService.js) 5') export default LoginService